/*
 * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.jmx.mbeanserver;


import static com.sun.jmx.defaults.JmxProperties.MBEANSERVER_LOGGER;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Permission;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.util.Map;
import java.util.logging.Level;

import javax.management.InstanceNotFoundException;
import javax.management.MBeanException;
import javax.management.MBeanPermission;
import javax.management.NotCompliantMBeanException;
import javax.management.ObjectName;
import javax.management.OperationsException;
import javax.management.ReflectionException;
import javax.management.RuntimeErrorException;
import javax.management.RuntimeMBeanException;
import javax.management.RuntimeOperationsException;
import sun.reflect.misc.ConstructorUtil;
import sun.reflect.misc.ReflectUtil;

/**
 * Implements the MBeanInstantiator interface. Provides methods for
 * instantiating objects, finding the class given its name and using
 * different class loaders, deserializing objects in the context of a
 * given class loader.
 *
 * @since 1.5
 */
public class MBeanInstantiator {

  private final ModifiableClassLoaderRepository clr;
  //    private MetaData meta = null;

  MBeanInstantiator(ModifiableClassLoaderRepository clr) {
    this.clr = clr;
  }


  /**
   * This methods tests if the MBean class makes it possible to
   * instantiate an MBean of this class in the MBeanServer.
   * e.g. it must have a public constructor, be a concrete class...
   */
  public void testCreation(Class<?> c) throws NotCompliantMBeanException {
    Introspector.testCreation(c);
  }

  /**
   * Loads the class with the specified name using this object's
   * Default Loader Repository.
   **/
  public Class<?> findClassWithDefaultLoaderRepository(String className)
      throws ReflectionException {

    Class<?> theClass;
    if (className == null) {
      throw new RuntimeOperationsException(new
          IllegalArgumentException("The class name cannot be null"),
          "Exception occurred during object instantiation");
    }

    ReflectUtil.checkPackageAccess(className);
    try {
      if (clr == null) {
        throw new ClassNotFoundException(className);
      }
      theClass = clr.loadClass(className);
    } catch (ClassNotFoundException ee) {
      throw new ReflectionException(ee,
          "The MBean class could not be loaded by the default loader repository");
    }

    return theClass;
  }


  /**
   * Gets the class for the specified class name using the MBean
   * Interceptor's classloader
   */
  public Class<?> findClass(String className, ClassLoader loader)
      throws ReflectionException {

    return loadClass(className, loader);
  }

  /**
   * Gets the class for the specified class name using the specified
   * class loader
   */
  public Class<?> findClass(String className, ObjectName aLoader)
      throws ReflectionException, InstanceNotFoundException {

    if (aLoader == null) {
      throw new RuntimeOperationsException(new
          IllegalArgumentException(), "Null loader passed in parameter");
    }

    // Retrieve the class loader from the repository
    ClassLoader loader = null;
    synchronized (this) {
      loader = getClassLoader(aLoader);
    }
    if (loader == null) {
      throw new InstanceNotFoundException("The loader named " +
          aLoader + " is not registered in the MBeanServer");
    }
    return findClass(className, loader);
  }


  /**
   * Return an array of Class corresponding to the given signature, using
   * the specified class loader.
   */
  public Class<?>[] findSignatureClasses(String signature[],
      ClassLoader loader)
      throws ReflectionException {

    if (signature == null) {
      return null;
    }
    final ClassLoader aLoader = loader;
    final int length = signature.length;
    final Class<?> tab[] = new Class<?>[length];

    if (length == 0) {
      return tab;
    }
    try {
      for (int i = 0; i < length; i++) {
        // Start handling primitive types (int. boolean and so
        // forth)
        //

        final Class<?> primCla = primitiveClasses.get(signature[i]);
        if (primCla != null) {
          tab[i] = primCla;
          continue;
        }

        ReflectUtil.checkPackageAccess(signature[i]);
        // Ok we do not have a primitive type ! We need to build
        // the signature of the method
        //
        if (aLoader != null) {
          // We need to load the class through the class
          // loader of the target object.
          //
          tab[i] = Class.forName(signature[i], false, aLoader);
        } else {
          // Load through the default class loader
          //
          tab[i] = findClass(signature[i],
              this.getClass().getClassLoader());
        }
      }
    } catch (ClassNotFoundException e) {
      if (MBEANSERVER_LOGGER.isLoggable(Level.FINEST)) {
        MBEANSERVER_LOGGER.logp(Level.FINEST,
            MBeanInstantiator.class.getName(),
            "findSignatureClasses",
            "The parameter class could not be found", e);
      }
      throw new ReflectionException(e,
          "The parameter class could not be found");
    } catch (RuntimeException e) {
      if (MBEANSERVER_LOGGER.isLoggable(Level.FINEST)) {
        MBEANSERVER_LOGGER.logp(Level.FINEST,
            MBeanInstantiator.class.getName(),
            "findSignatureClasses",
            "Unexpected exception", e);
      }
      throw e;
    }
    return tab;
  }


  /**
   * Instantiates an object given its class, using its empty constructor.
   * The call returns a reference to the newly created object.
   */
  public Object instantiate(Class<?> theClass)
      throws ReflectionException, MBeanException {

    checkMBeanPermission(theClass, null, null, "instantiate");

    Object moi;

    // ------------------------------
    // ------------------------------
    Constructor<?> cons = findConstructor(theClass, null);
    if (cons == null) {
      throw new ReflectionException(new
          NoSuchMethodException("No such constructor"));
    }
    // Instantiate the new object
    try {
      ReflectUtil.checkPackageAccess(theClass);
      ensureClassAccess(theClass);
      moi = cons.newInstance();
    } catch (InvocationTargetException e) {
      // Wrap the exception.
      Throwable t = e.getTargetException();
      if (t instanceof RuntimeException) {
        throw new RuntimeMBeanException((RuntimeException) t,
            "RuntimeException thrown in the MBean's empty constructor");
      } else if (t instanceof Error) {
        throw new RuntimeErrorException((Error) t,
            "Error thrown in the MBean's empty constructor");
      } else {
        throw new MBeanException((Exception) t,
            "Exception thrown in the MBean's empty constructor");
      }
    } catch (NoSuchMethodError error) {
      throw new ReflectionException(new
          NoSuchMethodException("No constructor"),
          "No such constructor");
    } catch (InstantiationException e) {
      throw new ReflectionException(e,
          "Exception thrown trying to invoke the MBean's empty constructor");
    } catch (IllegalAccessException e) {
      throw new ReflectionException(e,
          "Exception thrown trying to invoke the MBean's empty constructor");
    } catch (IllegalArgumentException e) {
      throw new ReflectionException(e,
          "Exception thrown trying to invoke the MBean's empty constructor");
    }
    return moi;

  }


  /**
   * Instantiates an object given its class, the parameters and
   * signature of its constructor The call returns a reference to
   * the newly created object.
   */
  public Object instantiate(Class<?> theClass, Object params[],
      String signature[], ClassLoader loader)
      throws ReflectionException, MBeanException {

    checkMBeanPermission(theClass, null, null, "instantiate");

    // Instantiate the new object
    // ------------------------------
    // ------------------------------
    final Class<?>[] tab;
    Object moi;
    try {
      // Build the signature of the method
      //
      ClassLoader aLoader = theClass.getClassLoader();
      // Build the signature of the method
      //
      tab =
          ((signature == null) ? null :
              findSignatureClasses(signature, aLoader));
    }
    // Exception IllegalArgumentException raised in Jdk1.1.8
    catch (IllegalArgumentException e) {
      throw new ReflectionException(e,
          "The constructor parameter classes could not be loaded");
    }

    // Query the metadata service to get the right constructor
    Constructor<?> cons = findConstructor(theClass, tab);

    if (cons == null) {
      throw new ReflectionException(new
          NoSuchMethodException("No such constructor"));
    }
    try {
      ReflectUtil.checkPackageAccess(theClass);
      ensureClassAccess(theClass);
      moi = cons.newInstance(params);
    } catch (NoSuchMethodError error) {
      throw new ReflectionException(new
          NoSuchMethodException("No such constructor found"),
          "No such constructor");
    } catch (InstantiationException e) {
      throw new ReflectionException(e,
          "Exception thrown trying to invoke the MBean's constructor");
    } catch (IllegalAccessException e) {
      throw new ReflectionException(e,
          "Exception thrown trying to invoke the MBean's constructor");
    } catch (InvocationTargetException e) {
      // Wrap the exception.
      Throwable th = e.getTargetException();
      if (th instanceof RuntimeException) {
        throw new RuntimeMBeanException((RuntimeException) th,
            "RuntimeException thrown in the MBean's constructor");
      } else if (th instanceof Error) {
        throw new RuntimeErrorException((Error) th,
            "Error thrown in the MBean's constructor");
      } else {
        throw new MBeanException((Exception) th,
            "Exception thrown in the MBean's constructor");
      }
    }
    return moi;
  }

  /**
   * De-serializes a byte array in the context of a classloader.
   *
   * @param loader the classloader to use for de-serialization
   * @param data The byte array to be de-sererialized.
   * @return The de-serialized object stream.
   * @throws OperationsException Any of the usual Input/Output related exceptions.
   */
  public ObjectInputStream deserialize(ClassLoader loader, byte[] data)
      throws OperationsException {

    // Check parameter validity
    if (data == null) {
      throw new RuntimeOperationsException(new
          IllegalArgumentException(), "Null data passed in parameter");
    }
    if (data.length == 0) {
      throw new RuntimeOperationsException(new
          IllegalArgumentException(), "Empty data passed in parameter");
    }

    // Object deserialization
    ByteArrayInputStream bIn;
    ObjectInputStream objIn;

    bIn = new ByteArrayInputStream(data);
    try {
      objIn = new ObjectInputStreamWithLoader(bIn, loader);
    } catch (IOException e) {
      throw new OperationsException(
          "An IOException occurred trying to de-serialize the data");
    }

    return objIn;
  }

  /**
   * De-serializes a byte array in the context of a given MBean class loader.
   * <P>The class loader is the one that loaded the class with name
   * "className".
   * <P>The name of the class loader to be used for loading the specified
   * class is specified. If null, a default one has to be provided (for a
   * MBean Server, its own class loader will be used).
   *
   * @param className The name of the class whose class loader should be used for the
   * de-serialization.
   * @param data The byte array to be de-sererialized.
   * @param loaderName The name of the class loader to be used for loading the specified class. If
   * null, a default one has to be provided (for a MBean Server, its own class loader will be
   * used).
   * @return The de-serialized object stream.
   * @throws InstanceNotFoundException The specified class loader MBean is not found.
   * @throws OperationsException Any of the usual Input/Output related exceptions.
   * @throws ReflectionException The specified class could not be loaded by the specified class
   * loader.
   */
  public ObjectInputStream deserialize(String className,
      ObjectName loaderName,
      byte[] data,
      ClassLoader loader)
      throws InstanceNotFoundException,
      OperationsException,
      ReflectionException {

    // Check parameter validity
    if (data == null) {
      throw new RuntimeOperationsException(new
          IllegalArgumentException(), "Null data passed in parameter");
    }
    if (data.length == 0) {
      throw new RuntimeOperationsException(new
          IllegalArgumentException(), "Empty data passed in parameter");
    }
    if (className == null) {
      throw new RuntimeOperationsException(new
          IllegalArgumentException(), "Null className passed in parameter");
    }

    ReflectUtil.checkPackageAccess(className);
    Class<?> theClass;
    if (loaderName == null) {
      // Load the class using the agent class loader
      theClass = findClass(className, loader);

    } else {
      // Get the class loader MBean
      try {
        ClassLoader instance = null;

        instance = getClassLoader(loaderName);
        if (instance == null) {
          throw new ClassNotFoundException(className);
        }
        theClass = Class.forName(className, false, instance);
      } catch (ClassNotFoundException e) {
        throw new ReflectionException(e,
            "The MBean class could not be loaded by the " +
                loaderName.toString() + " class loader");
      }
    }

    // Object deserialization
    ByteArrayInputStream bIn;
    ObjectInputStream objIn;

    bIn = new ByteArrayInputStream(data);
    try {
      objIn = new ObjectInputStreamWithLoader(bIn,
          theClass.getClassLoader());
    } catch (IOException e) {
      throw new OperationsException(
          "An IOException occurred trying to de-serialize the data");
    }

    return objIn;
  }


  /**
   * Instantiates an object using the list of all class loaders registered
   * in the MBean Interceptor
   * (using its {@link javax.management.loading.ClassLoaderRepository}).
   * <P>The object's class should have a public constructor.
   * <P>It returns a reference to the newly created object.
   * <P>The newly created object is not registered in the MBean Interceptor.
   *
   * @param className The class name of the object to be instantiated.
   * @return The newly instantiated object.
   * @throws ReflectionException Wraps a <CODE>java.lang.ClassNotFoundException</CODE> or the
   * <CODE>java.lang.Exception</CODE> that occurred when trying to invoke the object's constructor.
   * @throws MBeanException The constructor of the object has thrown an exception
   * @throws RuntimeOperationsException Wraps a <CODE>java.lang.IllegalArgumentException</CODE>: the
   * className passed in parameter is null.
   */
  public Object instantiate(String className)
      throws ReflectionException,
      MBeanException {

    return instantiate(className, (Object[]) null, (String[]) null, null);
  }


  /**
   * Instantiates an object using the class Loader specified by its
   * <CODE>ObjectName</CODE>.
   * <P>If the loader name is null, a default one has to be provided (for a
   * MBean Server, the ClassLoader that loaded it will be used).
   * <P>The object's class should have a public constructor.
   * <P>It returns a reference to the newly created object.
   * <P>The newly created object is not registered in the MBean Interceptor.
   *
   * @param className The class name of the MBean to be instantiated.
   * @param loaderName The object name of the class loader to be used.
   * @return The newly instantiated object.
   * @throws ReflectionException Wraps a <CODE>java.lang.ClassNotFoundException</CODE> or the
   * <CODE>java.lang.Exception</CODE> that occurred when trying to invoke the object's constructor.
   * @throws MBeanException The constructor of the object has thrown an exception.
   * @throws InstanceNotFoundException The specified class loader is not registered in the
   * MBeanServerInterceptor.
   * @throws RuntimeOperationsException Wraps a <CODE>java.lang.IllegalArgumentException</CODE>: the
   * className passed in parameter is null.
   */
  public Object instantiate(String className, ObjectName loaderName,
      ClassLoader loader)
      throws ReflectionException, MBeanException,
      InstanceNotFoundException {

    return instantiate(className, loaderName, (Object[]) null,
        (String[]) null, loader);
  }


  /**
   * Instantiates an object using the list of all class loaders registered
   * in the MBean server
   * (using its {@link javax.management.loading.ClassLoaderRepository}).
   * <P>The object's class should have a public constructor.
   * <P>The call returns a reference to the newly created object.
   * <P>The newly created object is not registered in the MBean Interceptor.
   *
   * @param className The class name of the object to be instantiated.
   * @param params An array containing the parameters of the constructor to be invoked.
   * @param signature An array containing the signature of the constructor to be invoked.
   * @return The newly instantiated object.
   * @throws ReflectionException Wraps a <CODE>java.lang.ClassNotFoundException</CODE> or the
   * <CODE>java.lang.Exception</CODE> that occurred when trying to invoke the object's constructor.
   * @throws MBeanException The constructor of the object has thrown an exception
   * @throws RuntimeOperationsException Wraps a <CODE>java.lang.IllegalArgumentException</CODE>: the
   * className passed in parameter is null.
   */
  public Object instantiate(String className,
      Object params[],
      String signature[],
      ClassLoader loader)
      throws ReflectionException,
      MBeanException {

    Class<?> theClass = findClassWithDefaultLoaderRepository(className);
    return instantiate(theClass, params, signature, loader);
  }


  /**
   * Instantiates an object. The class loader to be used is identified by its
   * object name.
   * <P>If the object name of the loader is null, a default has to be
   * provided (for example, for a MBean Server, the ClassLoader that loaded
   * it will be used).
   * <P>The object's class should have a public constructor.
   * <P>The call returns a reference to the newly created object.
   * <P>The newly created object is not registered in the MBean server.
   *
   * @param className The class name of the object to be instantiated.
   * @param params An array containing the parameters of the constructor to be invoked.
   * @param signature An array containing the signature of the constructor to be invoked.
   * @param loaderName The object name of the class loader to be used.
   * @return The newly instantiated object.
   * @throws ReflectionException Wraps a <CODE>java.lang.ClassNotFoundException</CODE> or the
   * <CODE>java.lang.Exception</CODE> that occurred when trying to invoke the object's constructor.
   * @throws MBeanException The constructor of the object has thrown an exception
   * @throws InstanceNotFoundException The specified class loader is not registered in the MBean
   * Interceptor.
   * @throws RuntimeOperationsException Wraps a <CODE>java.lang.IllegalArgumentException</CODE>: the
   * className passed in parameter is null.
   */
  public Object instantiate(String className,
      ObjectName loaderName,
      Object params[],
      String signature[],
      ClassLoader loader)
      throws ReflectionException,
      MBeanException,
      InstanceNotFoundException {

    // ------------------------------
    // ------------------------------
    Class<?> theClass;

    if (loaderName == null) {
      theClass = findClass(className, loader);
    } else {
      theClass = findClass(className, loaderName);
    }
    return instantiate(theClass, params, signature, loader);
  }


  /**
   * Return the Default Loader Repository used by this instantiator object.
   **/
  public ModifiableClassLoaderRepository getClassLoaderRepository() {
    checkMBeanPermission((String) null, null, null, "getClassLoaderRepository");
    return clr;
  }

  /**
   * Load a class with the specified loader, or with this object
   * class loader if the specified loader is null.
   **/
  static Class<?> loadClass(String className, ClassLoader loader)
      throws ReflectionException {
    Class<?> theClass;
    if (className == null) {
      throw new RuntimeOperationsException(new
          IllegalArgumentException("The class name cannot be null"),
          "Exception occurred during object instantiation");
    }
    ReflectUtil.checkPackageAccess(className);
    try {
      if (loader == null) {
        loader = MBeanInstantiator.class.getClassLoader();
      }
      if (loader != null) {
        theClass = Class.forName(className, false, loader);
      } else {
        theClass = Class.forName(className);
      }
    } catch (ClassNotFoundException e) {
      throw new ReflectionException(e,
          "The MBean class could not be loaded");
    }
    return theClass;
  }


  /**
   * Load the classes specified in the signature with the given loader,
   * or with this object class loader.
   **/
  static Class<?>[] loadSignatureClasses(String signature[],
      ClassLoader loader)
      throws ReflectionException {

    if (signature == null) {
      return null;
    }
    final ClassLoader aLoader =
        (loader == null ? MBeanInstantiator.class.getClassLoader() : loader);
    final int length = signature.length;
    final Class<?> tab[] = new Class<?>[length];

    if (length == 0) {
      return tab;
    }
    try {
      for (int i = 0; i < length; i++) {
        // Start handling primitive types (int. boolean and so
        // forth)
        //

        final Class<?> primCla = primitiveClasses.get(signature[i]);
        if (primCla != null) {
          tab[i] = primCla;
          continue;
        }

        // Ok we do not have a primitive type ! We need to build
        // the signature of the method
        //
        // We need to load the class through the class
        // loader of the target object.
        //
        ReflectUtil.checkPackageAccess(signature[i]);
        tab[i] = Class.forName(signature[i], false, aLoader);
      }
    } catch (ClassNotFoundException e) {
      if (MBEANSERVER_LOGGER.isLoggable(Level.FINEST)) {
        MBEANSERVER_LOGGER.logp(Level.FINEST,
            MBeanInstantiator.class.getName(),
            "findSignatureClasses",
            "The parameter class could not be found", e);
      }
      throw new ReflectionException(e,
          "The parameter class could not be found");
    } catch (RuntimeException e) {
      if (MBEANSERVER_LOGGER.isLoggable(Level.FINEST)) {
        MBEANSERVER_LOGGER.logp(Level.FINEST,
            MBeanInstantiator.class.getName(),
            "findSignatureClasses",
            "Unexpected exception", e);
      }
      throw e;
    }
    return tab;
  }

  private Constructor<?> findConstructor(Class<?> c, Class<?>[] params) {
    try {
      return ConstructorUtil.getConstructor(c, params);
    } catch (Exception e) {
      return null;
    }
  }


  private static final Map<String, Class<?>> primitiveClasses = Util.newMap();

  static {
    for (Class<?> c : new Class<?>[]{byte.class, short.class, int.class,
        long.class, float.class, double.class,
        char.class, boolean.class}) {
      primitiveClasses.put(c.getName(), c);
    }
  }

  private static void checkMBeanPermission(Class<?> clazz,
      String member,
      ObjectName objectName,
      String actions) {
    if (clazz != null) {
      checkMBeanPermission(clazz.getName(), member, objectName, actions);
    }
  }

  private static void checkMBeanPermission(String classname,
      String member,
      ObjectName objectName,
      String actions)
      throws SecurityException {
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      Permission perm = new MBeanPermission(classname,
          member,
          objectName,
          actions);
      sm.checkPermission(perm);
    }
  }

  private static void ensureClassAccess(Class clazz)
      throws IllegalAccessException {
    int mod = clazz.getModifiers();
    if (!Modifier.isPublic(mod)) {
      throw new IllegalAccessException("Class is not public and can't be instantiated");
    }
  }

  private ClassLoader getClassLoader(final ObjectName name) {
    if (clr == null) {
      return null;
    }
    // Restrict to getClassLoader permission only
    Permissions permissions = new Permissions();
    permissions.add(new MBeanPermission("*", null, name, "getClassLoader"));
    ProtectionDomain protectionDomain = new ProtectionDomain(null, permissions);
    ProtectionDomain[] domains = {protectionDomain};
    AccessControlContext ctx = new AccessControlContext(domains);
    ClassLoader loader = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
      public ClassLoader run() {
        return clr.getClassLoader(name);
      }
    }, ctx);
    return loader;
  }
}
